#
# Copyright (C) 2023, Inria
# GRAPHDECO research group, https://team.inria.fr/graphdeco
# All rights reserved.
#
# This software is free for non-commercial, research and evaluation use 
# under the terms of the LICENSE.md file.
#
# For inquiries contact  george.drettakis@inria.fr
#

import torch, math
from torch import nn
import torch.nn.functional as F
import numpy as np
from utils.graphics_utils import getWorld2View2, getProjectionMatrix

class Camera(nn.Module):
    def __init__(self, colmap_id, R, T, FoVx, FoVy, image, gt_alpha_mask,
                 image_name, uid, down_image=None,
                 trans=np.array([0.0, 0.0, 0.0]), scale=1.0, data_device = "cuda"
                 ):
        super(Camera, self).__init__()

        self.uid = uid
        self.colmap_id = colmap_id
        self.R = R
        self.T = T
        self.FoVx = FoVx
        self.FoVy = FoVy
        self.image_name = image_name

        try:
            self.data_device = torch.device(data_device)
        except Exception as e:
            print(e)
            print(f"[Warning] Custom device {data_device} failed, fallback to default cuda device" )
            self.data_device = torch.device("cuda")

        self.original_image = image.clamp(0.0, 1.0).to(self.data_device)
        self.image_width = self.original_image.shape[2]
        self.image_height = self.original_image.shape[1]
        self.down_image = down_image.clamp(0.0, 1.0).to(self.data_device)
        self.h, self.w = self.down_image.shape[1:]

        if gt_alpha_mask is not None:
            self.original_image *= gt_alpha_mask.to(self.data_device)
        else:
            self.original_image *= torch.ones((1, self.image_height, self.image_width), device=self.data_device)

        self.zfar = 100.0
        self.znear = 0.01

        self.trans = trans
        self.scale = scale

        self.world_view_transform = torch.tensor(getWorld2View2(R, T, trans, scale)).transpose(0, 1).cuda()
        self.projection_matrix = getProjectionMatrix(znear=self.znear, zfar=self.zfar, fovX=self.FoVx, fovY=self.FoVy).transpose(0,1).cuda()
        self.full_proj_transform = (self.world_view_transform.unsqueeze(0).bmm(self.projection_matrix.unsqueeze(0))).squeeze(0)
        self.camera_center = self.world_view_transform.inverse()[3, :3]
        self.plucker_rays = self.generate_plucker_rays().permute(1,2,0).reshape(-1, 6)
        
    def generate_plucker_rays(self):
        FoVx = torch.tensor(self.FoVx, device=self.data_device)    
        FoVy = torch.tensor(self.FoVy, device=self.data_device)

        i, j = torch.meshgrid(
            torch.arange(self.w, device=self.data_device),
            torch.arange(self.h, device=self.data_device),
            indexing='xy'
        )

        x = (i + 0.5 - self.w / 2) / (self.w / 2) * torch.tan(FoVx / 2)
        y = -(j + 0.5 - self.h / 2) / (self.h / 2) * torch.tan(FoVy / 2)

        dirs = torch.stack([x, y, -torch.ones_like(x)], dim=0)  # (3, H, W)
        dirs = dirs / torch.norm(dirs, dim=0, keepdim=True)

        R = torch.tensor(self.R, device=self.data_device, dtype=torch.float32)  # (3,3)
        d_world = R.T @ dirs.view(3, -1)  # (3, H*W)
        d_world = d_world / torch.norm(d_world, dim=0, keepdim=True)
        d_world = d_world.view(3, self.h, self.w)

        o_world = self.camera_center.to(self.data_device)  # (3,)
        o_world = o_world.view(3, 1, 1).expand(-1, self.h, self.w)

        m_world = torch.cross(o_world, d_world, dim=0)  # (3, H, W)
        plucker = torch.cat([d_world, m_world], dim=0)  # (6, H, W)
        return plucker
    

class MiniCam:
    def __init__(self, width, height, fovy, fovx, znear, zfar, world_view_transform, full_proj_transform):
        self.image_width = width
        self.image_height = height    
        self.FoVy = fovy
        self.FoVx = fovx
        self.znear = znear
        self.zfar = zfar
        self.world_view_transform = world_view_transform
        self.full_proj_transform = full_proj_transform
        view_inv = torch.inverse(self.world_view_transform)
        self.camera_center = view_inv[3][:3]

